package nr.co.mhgames.polyanim;

import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Locale;

import nr.co.mhgames.polyanim.interpolation.Interpolator;
import nr.co.mhgames.polyanim.interpolation.LinearInterpolator;
import nr.co.mhgames.polyanim.interpolation.PowerCurveInterpolator;
import nr.co.mhgames.polyanim.interpolation.SmoothStepInterpolator;
import nr.co.mhgames.polyanim.util.Matrix;
import nr.co.mhgames.polyanim.util.Vector2f;
import nr.co.mhgames.polyanim.util.Vertex;

/**
 * Animation, consisting of several key frames and their transition durations
 * (in seconds) along the animation time line. The total animation length is
 * computed from the frame durations (see {@link #calculateLength()}). Looped
 * animations are also supported with bidirectional play back.
 * 
 * @author Mika Halttunen
 */
public class Animation {
	/** All the possible animation states */
	public enum AnimationState {
		STOPPED, PLAYING
	}

	/** Animation playing directions */
	public enum AnimationDirection {
		FORWARD, BACKWARD
	}

	/** Animation playing style */
	public enum AnimationStyle {
		NORMAL, PINGPONG
	}

	/** Animation name */
	protected String name;

	/** Lazy updates enabled? */
	protected boolean lazyUpdates;

	/** Key frames with their durations */
	protected ArrayList<TimedKeyFrame> frames;

	/** Current frame */
	protected KeyFrame currentFrame;

	/** Current animation state */
	protected AnimationState state;

	/** Playing direction */
	protected AnimationDirection direction;

	/** Playing style */
	protected AnimationStyle playstyle;

	/** Current position on the time line */
	protected float position;

	/** Animation length in seconds */
	protected float length;

	/** Interpolator to use between the key frames */
	protected Interpolator interpolator;

	/** Loop starting position. Default is the animation start (0.0). */
	protected float loopStartPosition;

	/** Loop ending position Default is the animation length. */
	protected float loopEndPosition;

	/**
	 * Loop count. Anything <b>below zero</b> means loop infinitely, zero means
	 * play once, 1 means play twice (loop once), etc.
	 */
	protected int loopCount;

	/** Amount of loops made so far */
	protected int loopsMade;


	/** Bounding rectangle (computed in computeBounds()). */
	protected Rectangle2D.Float boundingRect;


	// These are for update()-method, helping it to keep track of things
	private int lastFrameIndex;
	private int currentFrameIndex;
	private float framePosition;


	/** Default constructor */
	public Animation() {
		frames = new ArrayList<TimedKeyFrame>();
		name = "null";
		currentFrame = null;
		state = AnimationState.STOPPED;
		direction = AnimationDirection.FORWARD;
		playstyle = AnimationStyle.NORMAL;

		position = 0;
		length = 0;
		lazyUpdates = false;
		interpolator = LinearInterpolator.instance;

		loopStartPosition = loopEndPosition = 0;
		loopCount = 0; // Play once by default
		loopsMade = 0;

		lastFrameIndex = 0;
		currentFrameIndex = 0;
		framePosition = 0;
	}

	/** Constructor */
	public Animation(String name) {
		this();
		this.name = name;
	}


	/** Return the animation name */
	public String getName() {
		return name;
	}

	/** Set the animation name */
	public void setName(String name) {
		this.name = name;
	}


	/**
	 * Add a key frame to the animation.
	 * 
	 * @param frame
	 *            Key frame to add (contains the duration)
	 */
	public void addKeyFrame(TimedKeyFrame frame) {
		frames.add(frame);
		calculateLength();

		// If this was the first frame, set the current frame
		if (currentFrame == null) {
			currentFrame = frame.getFrame();
		}
	}

	/**
	 * Add a key frame to the animation with the given duration.
	 * 
	 * @param frame
	 *            Key frame to add
	 * @param duration
	 *            Frame duration in seconds
	 */
	public void addKeyFrame(KeyFrame frame, float duration) {
		addKeyFrame(new TimedKeyFrame(frame, duration));
	}

	/**
	 * Add a key frame to the animation. Its duration will be zero. This should
	 * be used for the first frame only.
	 * 
	 * @param frame
	 *            Key frame to add
	 */
	public void addKeyFrame(KeyFrame frame) {
		addKeyFrame(new TimedKeyFrame(frame, 0));
	}

	/**
	 * Return the key frame at given index.
	 * 
	 * @param i
	 *            Key frame index
	 * @return Frame at the index specified
	 */
	public TimedKeyFrame getKeyFrame(int i) {
		assert (i >= 0 && i < getFrameCount());
		return frames.get(i);
	}

	/**
	 * Return the key frame duration (in seconds) for the key frame at the given
	 * index.
	 * 
	 * @param i
	 *            Frame index
	 * @return Frame duration in seconds
	 */
	public float getKeyFrameDuration(int i) {
		assert (i >= 0 && i < getFrameCount());
		return frames.get(i).duration;
	}

	/**
	 * Return the list of the key frames.
	 * 
	 * @return List of frames
	 */
	public ArrayList<TimedKeyFrame> getKeyFrames() {
		return frames;
	}

	/**
	 * Set the given key frame to the specified index.
	 * 
	 * @param i
	 *            Frame index
	 * @param frame
	 *            New frame
	 */
	public void setKeyFrame(int i, TimedKeyFrame frame) {
		assert (i >= 0 && i < getFrameCount());
		frames.set(i, frame);
		calculateLength();
	}

	/**
	 * Return the current animation frame. It can be used in rendering, for
	 * example.
	 * 
	 * @return The current animation frame
	 */
	public KeyFrame getCurrentFrame() {
		return currentFrame;
	}


	/**
	 * Return the current animation state.
	 */
	public AnimationState getState() {
		return state;
	}

	/**
	 * Set the animation state.
	 * 
	 * @param state
	 *            New state
	 */
	protected void setState(AnimationState state) {
		this.state = state;
	}

	/**
	 * Return the current playing style.
	 */
	public AnimationStyle getPlayingStyle() {
		return playstyle;
	}

	/**
	 * Set the animation playing style.
	 * 
	 * @param style
	 *            New playing style
	 */
	public void setPlayingStyle(AnimationStyle style) {
		playstyle = style;
	}


	/** Return the current animation position in seconds */
	public float getPosition() {
		return position;
	}

	/**
	 * Set the animation position in seconds. This automatically computes the
	 * currently visible key frame.
	 * 
	 * @param pos
	 *            Position in seconds, between 0.0 and animation length.
	 * @see getLength()
	 */
	public void setPosition(float pos) {
		assert (pos >= 0 && pos <= length);

		if (pos != position) {
			position = pos;

			// Find the right frame
			float duration = 0;
			for (int i = 0; i < getFrameCount(); i++) {
				if (pos <= duration + getKeyFrameDuration(i)) {
					// We're at frame i
					lastFrameIndex = Math.max(0, i - 1);
					currentFrameIndex = i;
					framePosition = pos - duration;

					// Interpolate the right frame
					computeCurrentFrame();
					break;
				}

				duration += getKeyFrameDuration(i);
			}
		}
	}

	/**
	 * Return the currently assigned interpolator. By default, animations use
	 * the linear interpolator ({@link LinearInterpolator#instance})
	 * 
	 * @return Interpolator instance
	 */
	public Interpolator getInterpolator() {
		return interpolator;
	}

	/**
	 * Set the interpolator to use when interpolating between the key frames. By
	 * default, animations use the linear interpolator (
	 * {@link LinearInterpolator#instance}). PolyAnim comes with two additional
	 * interpolators, that may look better in some animations. These are
	 * {@link PowerCurveInterpolator} and {@link SmoothStepInterpolator}. See
	 * the respective classes for documentation. When you want
	 * {@link LinearInterpolator} or {@link SmoothStepInterpolator}, you can use
	 * the static instances provided in the classes.
	 * 
	 * @param interpolator
	 *            Interpolator to use
	 * @see {@link LinearInterpolator}, {@link PowerCurveInterpolator},
	 *      {@link SmoothStepInterpolator}
	 */
	public void setInterpolator(Interpolator interpolator) {
		this.interpolator = interpolator;
	}

	/**
	 * Calculates the animation length. It's the total length of the key frames.
	 */
	public void calculateLength() {
		length = 0;
		for (TimedKeyFrame frame: frames)
			length += frame.getDuration();
	}

	/** Return the animation length */
	public float getLength() {
		return length;
	}

	/** Return the loop starting position */
	public float getLoopStart() {
		return loopStartPosition;
	}

	/**
	 * Set the loop starting position. If the given position is after the loop
	 * ending position, the loop points are reversed.
	 */
	public void setLoopStart(float pos) {
		if (pos < 0)
			pos = 0;
		else if (pos > loopEndPosition && loopEndPosition > 0) {
			// Reverse the positions
			loopStartPosition = loopEndPosition;
			loopEndPosition = pos;
			return;
		}

		loopStartPosition = pos;
	}

	/** Return the loop ending position */
	public float getLoopEnd() {
		return loopEndPosition;
	}

	/**
	 * Set the loop ending position. If the given position is before the loop
	 * starting position, the loop points are reversed.
	 */
	public void setLoopEnd(float pos) {
		if (pos > length && length > 0)
			pos = length;
		else if (pos < loopStartPosition && pos > 0) {
			// Reverse the positions
			loopEndPosition = loopStartPosition;
			loopStartPosition = pos;
			return;
		}

		loopEndPosition = pos;
	}


	/**
	 * Set the looping count. This controls how many times the animation is
	 * played, i.e. the animation loops the specified times. 0 (zero) means play
	 * once, 1 means play twice (loop once), etc. Anything below zero means loop
	 * infinitely.
	 */
	public void setLoopCount(int loops) {
		loopCount = loops;
	}

	/** Return the looping count */
	public int getLoopCount() {
		return loopCount;
	}

	/** Return true if the animation is looping */
	public boolean isLooping() {
		return loopCount != 0;
	}

	/** Return the times the animation has looped so far */
	public int getTimesLooped() {
		return loopsMade;
	}


	/** Return the amount of key frames in the animation. */
	public int getFrameCount() {
		return frames.size();
	}

	/** Return the animation playing direction */
	public AnimationDirection getDirection() {
		return direction;
	}

	/** Set the animation playing direction */
	public void setDirection(AnimationDirection direction) {
		this.direction = direction;
	}


	/**
	 * Start playing the animation from the current position in the current
	 * direction.
	 * 
	 * @param times
	 *            How many times to play? Zero means loop infinitely.
	 */
	public void startPlaying(int times) {
		setLoopCount(times - 1);
		loopsMade = 0;
		setState(AnimationState.PLAYING);
	}

	/**
	 * Start playing the animation from the current position in the current
	 * direction.
	 */
	public void startPlaying() {
		setState(AnimationState.PLAYING);
	}

	/** Return true if the animation is playing */
	public boolean isPlaying() {
		return getState() == AnimationState.PLAYING;
	}

	/**
	 * Restart the animation. This is a convenience method that just calls
	 * {@link #rewind()} followed by {@link #startPlaying()}.
	 */
	public void restart() {
		rewind();
		startPlaying();
	}

	/**
	 * Rewind the animation to the beginning. This stops playing as well, you
	 * need to call {@link #startPlaying()} to restart the animation.
	 */
	public void rewind() {
		position = 0;
		currentFrameIndex = 0;
		lastFrameIndex = 0;
		framePosition = 0;
		loopsMade = 0;
		stopPlaying();
		setDirection(AnimationDirection.FORWARD);

		currentFrame = getKeyFrame(0).frame;
	}

	/** Stops playing the animation */
	public void stopPlaying() {
		setState(AnimationState.STOPPED);
	}


	/**
	 * Updates the (playing) animation by the given time delta value (in
	 * seconds). This advances the animation further and updates the current
	 * frame, which can then be rendered.
	 * 
	 * @param timeDelta
	 *            Time delta value since the last update, in seconds
	 */
	public void update(float timeDelta) {
		assert (frames != null && frames.size() > 0);
		// If there's no change, or the animation has only one frame: do nothing
		if (timeDelta == 0 || getFrameCount() == 1)
			return;

		// If the animation is not playing, do nothing
		if (getState() != AnimationState.PLAYING)
			return;

		if (getDirection() == AnimationDirection.FORWARD) {
			// Playing Forwards
			position += timeDelta;
			framePosition += timeDelta;

			// Loop end position reached? Animation has to be played
			// once in its entirety, though.
			if (loopsMade > 0 && getLoopEnd() > 0 && position >= getLoopEnd()) {
				handleLooping(true);
				return;
			}

			// Time to change a frame?
			float duration = getKeyFrameDuration(currentFrameIndex);
			if (framePosition > duration) {
				// Advance by a frame
				lastFrameIndex = currentFrameIndex;
				currentFrameIndex++;

				framePosition -= duration;

				// The last frame reached?
				if (currentFrameIndex >= getFrameCount()) {
					handleLooping(false);
					return;
				}
			}

		} else {
			// Playing Backwards
			position -= timeDelta;
			framePosition -= timeDelta;

			// Loop start position reached? Animation has to be played once in
			// its entirety, though. Worth noting that since we're playing
			// backwards, the loop start is effectively the loop end, and vice
			// versa.
			if (loopsMade > 0 && getLoopStart() > 0 && position <= getLoopStart()) {
				handleLooping(true);
				return;
			}

			// Time to change a frame?
			if (framePosition < 0) {
				// "Advance" to the previous frame
				currentFrameIndex--;
				lastFrameIndex = currentFrameIndex - 1;
				if (lastFrameIndex < 0)
					lastFrameIndex = 0;

				// The first frame reached?
				if (currentFrameIndex < 0) {
					handleLooping(false);
					return;
				} else
					framePosition += getKeyFrameDuration(currentFrameIndex);
			}
		}

		// Compute the current frame, unless lazy updates are enabled
		if (!lazyUpdates)
			computeCurrentFrame();
	}


	/**
	 * Handles the looping when the animation has reached either its end or the
	 * loop end marker.
	 * 
	 * @param reachedEndPoint
	 *            Set to true if the loop end position was reached, false means
	 *            we've reached the last animation frame (i.e. the end of the
	 *            animation)
	 * @remark Helper method for {@link #update(float)}
	 */
	private void handleLooping(boolean reachedEndPoint) {
		boolean playingForward = getDirection() == AnimationDirection.FORWARD;

		// If we're playing in ping pong style, reverse the direction
		if (loopCount != 0 && getPlayingStyle() == AnimationStyle.PINGPONG) {
			setDirection(playingForward ? AnimationDirection.BACKWARD : AnimationDirection.FORWARD);
			playingForward = !playingForward;
		}
		float loopPos = playingForward ? loopStartPosition : loopEndPosition;

		// Should we loop or not?
		if (loopCount == 0) {
			// No looping, just stop at the current frame
			stopPlaying();
		} else if (loopCount < 0) {
			// Infinite looping
			loopsMade++;

			if (loopPos == 0) {
				// Restart from the beginning
				if (playingForward)
					restart();
				else {
					// Restart from the end
					setPosition(getLength());
				}
			} else {
				// Start from the loop marker
				setPosition(loopPos);
			}
		} else {
			// Normal looping
			loopsMade++;
			if (loopsMade > loopCount) {
				// Required amount of loops done: stop at the current frame
				stopPlaying();
			} else {
				// Not enough loops yet, start again..
				if (loopPos == 0) {
					// Start from the beginning
					if (playingForward) {
						position = 0;
						currentFrameIndex = lastFrameIndex = 0;
						framePosition = 0;
						currentFrame = getKeyFrame(0).frame;
					} else {
						// Start from the end
						setPosition(getLength());
					}
				} else {
					// Start from the loop marker
					setPosition(loopPos);
				}
			}
		}
	}

	/** Compute the current frame by interpolating of two key frames. */
	private void computeCurrentFrame() {
		// Apply the interpolator
		float relPos = interpolator.interpolate(framePosition,
				getKeyFrameDuration(currentFrameIndex));

		// Interpolate between the frames
		KeyFrame lastFrame = getKeyFrame(lastFrameIndex).frame;
		KeyFrame nextFrame = getKeyFrame(currentFrameIndex).frame;
		currentFrame = KeyFrame.interpolate(lastFrame, nextFrame, relPos);
	}


	/**
	 * Returns whether the lazy updates are enabled or not. For explanation of
	 * lazy updates, refer to {@link #enableLazyUpdates(boolean)}.
	 * 
	 * @see #enableLazyUpdates(boolean)
	 */
	public boolean hasLazyUpdates() {
		return lazyUpdates;
	}

	/**
	 * Enables / disables the lazy updates. Lazy updates mean that while the
	 * animation state is updated normally, the frames are not interpolated,
	 * which saves CPU time.
	 * <p>
	 * Generally, in game context, you'd enable lazy updates for animations that
	 * are not visible in the screen, or are far away from the player. When the
	 * animation is coming back to sight, disable the lazy updates so the
	 * current frame will be computed.
	 */
	public void enableLazyUpdates(boolean enabled) {
		lazyUpdates = enabled;
	}


	/**
	 * Compute the bounding rectangle for the animation. Note that the animation
	 * must be set up ready for playing prior calling {@link #getBounds()} /
	 * {@link #computeBounds()} for the first time!
	 * 
	 * @remark The bounds generated may not be 100% accurate, depending on the
	 *         animation, since the bounds are computed via iterating through
	 *         the animation in discrete time steps.
	 */
	public void computeBounds() {
		boundingRect = new Rectangle2D.Float();

		// Play through the animation in time steps and keep enlarging the
		// bounding rectangle
		float step = 0.1f;
		restart();
		for (float time = 0; time <= length; time += step) {
			// Update the current frame
			update(step);

			// Calculate the bounds:
			// For each group..
			for (Group g: currentFrame.getGroups().values()) {
				// For each polygon in the group..
				Matrix tr = g.computeTransforms();
				for (Polygon p: g.getPolygons()) {
					// For each vertex in the polygon..
					for (Vertex v: p.getVertices()) {
						// Transform the vertex
						Vector2f vert = tr.transformPoint(v.position);
						boundingRect.add(vert.x, vert.y);
					}
				}
			}
		}

		// Done!
		rewind();
	}

	/**
	 * Return the bounding rectangle of the animation. If it has not been yet
	 * computed, {@link #computeBounds()} will be called. Note that the
	 * animation must be set up ready for playing prior calling
	 * {@link #getBounds()} / {@link #computeBounds()} for the first time!
	 * 
	 * @return The bounding rectangle enclosing the animation
	 */
	public Rectangle2D.Float getBounds() {
		if (boundingRect == null)
			computeBounds();
		return boundingRect;
	}


	/**
	 * Checks that each corresponding polygon group in adjacent key frames have
	 * the same amount of polygons. Throws an exception if that isn't the case.
	 * 
	 * @throws Exception
	 *             Thrown if the animation is not correct
	 */
	public void checkForFaults() throws Exception {
		for (int i = 0; i < getFrameCount(); i++) {
			int prev = i - 1;
			int next = i + 1;
			KeyFrame frame = getKeyFrame(i).getFrame();

			// FIXME: Clean this code up..

			// Check the next frame
			if (next < getFrameCount()) {
				KeyFrame nextFrame = getKeyFrame(next).getFrame();
				for (Group g: frame.getGroups().values()) {
					String name = g.getName();
					Group nextG = nextFrame.getGroup(name);
					if (nextG != null) {
						if (g.getPolygonCount() != nextG.getPolygonCount())
							throw new Exception(String.format("Error in animation '%s':\n"
									+ "Group '%s' has %d polygons in frame %d,"
									+ " and %d polygons in frame %d!", getName(), name, g
									.getPolygonCount(), i + 1, nextG.getPolygonCount(), next + 1));

						// Compare polygons
						try {
							Group.checkForFaults(g, nextG);
						} catch (Exception e) {
							throw new Exception(String.format(
									"Error in animation '%s', frames %d and %d:", getName(), i + 1,
									next + 1), e);
						}
					}

				}
			}

			// Check the previous frame
			if (prev >= 0) {
				KeyFrame prevFrame = getKeyFrame(prev).getFrame();
				for (Group g: frame.getGroups().values()) {
					String name = g.getName();
					Group prevG = prevFrame.getGroup(name);
					if (prevG != null) {
						if (g.getPolygonCount() != prevG.getPolygonCount())
							throw new Exception(String.format("Error in animation '%s':\n"
									+ "Group '%s' has %d polygons in frame %d,"
									+ " and %d polygons in frame %d!", getName(), name, g
									.getPolygonCount(), i + 1, prevG.getPolygonCount(), prev + 1));

						// Compare polygons
						try {
							Group.checkForFaults(g, prevG);
						} catch (Exception e) {
							throw new Exception(String.format(
									"Error in animation '%s', frames %d and %d:", getName(), i + 1,
									prev + 1), e);
						}
					}
				}
			}

		}
	}


	/**
	 * Creates a so called shared copy of the animation. The copy will have it's
	 * own state, position, etc. but it shares the key frames of the original
	 * animation. This saves memory and enables you to have several copies of an
	 * animation, each playing in their own pace as needed.
	 * <p>
	 * For example, suppose you have several enemies that have the same
	 * animation. You'd keep the original animation somewhere, and let all the
	 * enemy entities share it, using this method.
	 * 
	 * @return Animation instance that shares its key frames with this animation
	 */
	public Animation sharedCopy() {
		Animation copy = new Animation(name);
		copy.boundingRect = boundingRect;

		copy.currentFrameIndex = currentFrameIndex;
		copy.lastFrameIndex = lastFrameIndex;
		copy.framePosition = framePosition;

		copy.length = length;
		copy.direction = direction;

		copy.loopCount = loopCount;
		copy.loopEndPosition = loopEndPosition;
		copy.loopStartPosition = loopStartPosition;
		copy.loopsMade = loopsMade;
		copy.playstyle = playstyle;
		copy.position = position;
		copy.state = state;
		copy.lazyUpdates = lazyUpdates;

		// Here's the important bit, the copy shares the animation frames
		copy.frames = frames;

		// Calculate the current frame for the copy
		copy.computeCurrentFrame();

		return copy;
	}


	@Override
	public String toString() {
		return String.format((Locale) null, "Animation %s: length %.2fs, %d key frames, %s",
				getName(), getLength(), getFrameCount(), super.toString());
	}
}
